/**
 * MiniView.java Created on : 1 juin 07
 */
package fr.limsi.seven;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map.Entry;

import javax.swing.JPanel;

/**
 * @author Camille
 */
public class MiniView2D extends JPanel {

	/**
	 * Auto-generated serial version UID number
	 */
	private static final long serialVersionUID = -344670966250202816L;

	/**
	 * Mini viewer margins
	 */
	public static final int MARGINS = 5;

	/**
	 * Mini viewer visible area borders
	 */
	public static final int BORDERS = 1;

	/**
	 * Links shape
	 */
	public static final int TRIANGULAR_LINKS = 0;

	/**
	 * Links shape
	 */
	public static final int TRAPEZOIDAL_LINKS = 1;

	/**
	 * Trapezoid top edge width delta (in pixel) in ratio to the bottom edge
	 */
	public static final int TRAPEZOID_DELTA = 1;

	/**
	 * Current visible area color
	 */
	protected Color sightFieldColor = Color.BLUE;

	/**
	 * Fog
	 */
	public static final Color FOG = new Color(0.7f, 0.7f, 0.7f, 0.8f);

	/**
	 * Current drawing color
	 */
	protected Color drawingColor = Color.BLACK;

	/**
	 * Default mini view width
	 */
	public static final int DEFAULT_WIDTH = PyramideViewer2D.DEFAULT_WIDTH / 4;

	/**
	 * Default mini view height
	 */
	public static final int DEFAULT_HEIGHT = PyramideViewer2D.DEFAULT_WIDTH / 4;

	/**
	 * Default mini view node width
	 */
	public static final int NODE_WIDTH = 4;

	/**
	 * Mouse x coordinate (used for dragging operation)
	 */
	protected int mouseX;

	/**
	 * Mouse y coordinate (used for dragging operation)
	 */
	protected int mouseY;

	/**
	 * Associated {@link Pyramide2D}
	 */
	protected Pyramide2D pyramide2D;

	/**
	 * Mini view sight field (corresponds to the user vision field on the {@link Pyramide2D})
	 */
	protected Rectangle2D sightField;

	/**
	 * The mini view base {@link MiniNode2D}
	 */
	protected LinkedList<MiniNode2D> base;

	/**
	 * The mini view {@link MiniNode2D} organized by levels
	 */
	protected LinkedList<LinkedList<MiniNode2D>> levels;

	/**
	 * {@link HashMap} associating all the view {@link Node2D} to mini view {@link MiniNode2D}
	 */
	protected HashMap<Node2D, MiniNode2D> assocNodes;

	/**
	 * Default constructor
	 * 
	 * @param view
	 *            the associated {@link Pyramide2D}
	 */
	public MiniView2D(Pyramide2D view) {

		super();

		setDoubleBuffered(true);
		setOpaque(true);
		setPreferredSize(new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT));

		this.pyramide2D = view;
		view.setMiniView(this);

		base = new LinkedList<MiniNode2D>();
		levels = new LinkedList<LinkedList<MiniNode2D>>();
		assocNodes = new HashMap<Node2D, MiniNode2D>();

		buildMiniView();
		buildInteractions();
	}

	/**
	 * Builds the mini view (creates {@link MiniNode2D} and associates them to the view {@link Node2D})
	 */
	protected void buildMiniView() {

		for (Node2D node2D : pyramide2D.getBase()) {
			MiniNode2D miniNode = new MiniNode2D(node2D);

			base.add(miniNode);
			assocNodes.put(node2D, miniNode);
		}

		for (LinkedList<Node2D> level : pyramide2D.getAllLevels()) {

			LinkedList<MiniNode2D> miniLevel = new LinkedList<MiniNode2D>();

			for (Node2D node2D : level) {
				MiniNode2D miniNode = new MiniNode2D(node2D);

				miniLevel.add(miniNode);
				assocNodes.put(node2D, miniNode);
			}

			levels.add(miniLevel);
		}

		/* Positions the nodes */
		positionNodes();
	}

	/**
	 * Creates and adds all the mini view mouse interactions
	 */
	protected void buildInteractions() {
		MouseAdapter mA = new MouseAdapter() {

			@Override
			public void mouseEntered(MouseEvent e) {
				setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
			}

			@Override
			public void mouseExited(MouseEvent e) {
				setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
			}

			@Override
			public void mousePressed(MouseEvent e) {
				mouseX = e.getX();
				mouseY = e.getY();

				if(!sightField.contains(mouseX, mouseY)){
					pyramide2D.getAnimationMonitor().queueAnimation(pyramide2D.animationBuilder.getTranslationAnimation(new Point((-(pyramide2D.getWidth() / DEFAULT_WIDTH) * mouseX) + (pyramide2D.getWidth() / 2), (-(pyramide2D.getHeight() / DEFAULT_HEIGHT) * mouseY) + (pyramide2D.getHeight() / 2))));
				}
			}

			@Override
			public void mouseWheelMoved(MouseWheelEvent e) {

				int direction = e.getWheelRotation();

				if (direction < 0) { // Wheel up
					pyramide2D.zoomIn();
				}
				else { // Wheel down
					pyramide2D.zoomOut();
				}
			}

			@Override
			public void mouseDragged(MouseEvent e) {

				if (pyramide2D.getAnimationMonitor().getCurrentAnimation() == null || pyramide2D.getAnimationMonitor().getCurrentAnimation().getType() != Animation.TRANSLATION_ANIM) {
					pyramide2D.setTranslation(new Point(pyramide2D.getTranslation().x + (-(e.getX() - mouseX) * (pyramide2D.getWidth() / DEFAULT_WIDTH)), pyramide2D.getTranslation().y + (-(e.getY() - mouseY) * (pyramide2D.getHeight() / DEFAULT_HEIGHT))));
					pyramide2D.repaint();

					mouseX = e.getX();
					mouseY = e.getY();

					pyramide2D.positionSecondaryObjects();
				}
			}
		};

		addMouseListener(mA);
		addMouseMotionListener(mA);
		addMouseWheelListener(mA);
	}

	@Override
	protected void paintComponent(Graphics g) {

		Graphics2D g2 = (Graphics2D) g;

		g2.setPaint(Color.WHITE);
		g2.fillRect(0, 0, getWidth(), getHeight());

		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);

		/* Drawing */
		draw(g2);
		paintChildren(g2);

		g2.dispose();
	}

	/**
	 * Draw the view
	 * 
	 * @param g2
	 *            the Graphics2D element used for the painting
	 */
	protected void draw(Graphics2D g2) {

		g2.setColor(drawingColor);

		/* Rendering nodes */
		for (MiniNode2D node : base) {
			g2.setPaint(node.getAssociatedNode().getInnerColor());
			g2.fill(new Ellipse2D.Float(node.getPosition().x, node.getPosition().y, NODE_WIDTH, NODE_WIDTH));

			g2.setPaint(node.getAssociatedNode().getColor());
			g2.draw(new Ellipse2D.Float(node.getPosition().x, node.getPosition().y, NODE_WIDTH, NODE_WIDTH));
		}

		for (LinkedList<MiniNode2D> level : levels) {
			for (MiniNode2D node : level) {
				g2.setPaint(node.getAssociatedNode().getInnerColor());
				g2.fill(new Ellipse2D.Float(node.getPosition().x, node.getPosition().y, NODE_WIDTH, NODE_WIDTH));

				g2.setPaint(node.getAssociatedNode().getColor());
				g2.draw(new Ellipse2D.Float(node.getPosition().x, node.getPosition().y, NODE_WIDTH, NODE_WIDTH));
			}
		}

		/* Tracing Links */
		for (Iterator<Entry<Node2D, MiniNode2D>> iter = assocNodes.entrySet().iterator(); iter.hasNext();) {

			Entry<Node2D, MiniNode2D> entry = iter.next();
			Node2D node2D = entry.getKey();
			MiniNode2D mininode = entry.getValue();

			g2.setPaint(node2D.getColor());
			traceLinks(g2, node2D.getPNode(), mininode.getPosition().x, mininode.getPosition().y);
		}

		/* Drawing visible area */
		updateVisibleArea();

		g2.setPaint(sightFieldColor);
		g2.setStroke(new BasicStroke(2.0f));
		g2.draw(sightField);

		/* Drawing fog */
		Rectangle2D.Float fog = new Rectangle2D.Float(0, 0, getWidth(), getHeight());
		Area fogArea = new Area(fog);
		fogArea.subtract(new Area(sightField));
		g2.setClip(fogArea);

		g2.setPaint(FOG);
		g2.fill(fog);
	}

	/**
	 * Updates the visible area
	 */
	protected void updateVisibleArea() {
		float x = 0;
		float y = 0;
		float dx = 0;
		float dy = 0;
		float width = DEFAULT_WIDTH;
		float height = DEFAULT_HEIGHT;

		/* ------ Position ------ */
		/* Translation influence */
		dx = -(pyramide2D.getTranslation().x / (pyramide2D.getWidth() / DEFAULT_WIDTH));
		dy = -(pyramide2D.getTranslation().y / (pyramide2D.getHeight() / DEFAULT_HEIGHT));

		x = dx;
		y = dy;

		/* Scale influence */
		dx = (DEFAULT_WIDTH / 2) * (1 - (1 / pyramide2D.getScale()));
		dy = (DEFAULT_HEIGHT / 2) * (1 - (1 / pyramide2D.getScale()));

		x += dx;
		y += dy;

		width *= (1 / pyramide2D.getScale());
		height *= (1 / pyramide2D.getScale());

		/* ------ Update ------ */
		sightField = new Rectangle2D.Float(x + BORDERS, y + BORDERS, width - 2 * BORDERS, height - 2 * BORDERS);
	}

	/**
	 * (Re)position all the {@link MiniNode2D}
	 */
	public void positionNodes() {

		float x;
		float y;

		/* 1 - Base level repositioning */
		float spacing = (DEFAULT_WIDTH - (2 * MARGINS) - (base.size() * NODE_WIDTH)) / (base.size() - 1);
		x = MARGINS; // first base node x coordinate

		for (MiniNode2D node : base) {

			y = getHeight() - MARGINS;
			node.setPosition((int) x, (int) y);

			x += NODE_WIDTH + spacing; // Increasing x coordinate for next node
		}

		/* 2.2 - Other levels repositioning */
		double pyramideHeight = pyramide2D.getPyramideHeight();

		for (LinkedList<MiniNode2D> level : levels) {
			for (MiniNode2D node : level) {

				try {

					PyramideNode pNode = node.getAssociatedNode().getPNode();

					x = calculateX(pNode);

					if (pyramide2D.getGrid().getScaleType() == Grid2D.LOGARITHMIC_SCALE) {
						y = (DEFAULT_HEIGHT - MARGINS) - ((float) (Math.log1p(pNode.getHeight() * pyramide2D.getGrid().getRangeFactor()) / Math.log1p(pyramideHeight * pyramide2D.getGrid().getRangeFactor())) * (DEFAULT_HEIGHT - (2 * MARGINS)));
					}
					else {
						y = ((DEFAULT_HEIGHT - MARGINS) - ((float) (pNode.getHeight() / (pyramideHeight)) * (DEFAULT_HEIGHT - (2 * MARGINS))));
					}

					node.setPosition((int) x, (int) y);
				}
				catch (PyramideViewer2DException e) {
					e.printStackTrace();
				}
			}
		}

		repaint();
	}

	/**
	 * Calculates the node x coordinate
	 * 
	 * @param node
	 * @return the middle value of the parents x coordinates
	 */
	protected float calculateX(PyramideNode node) throws PyramideViewer2DException {

		float xMin = Float.POSITIVE_INFINITY;
		float xMax = Float.NEGATIVE_INFINITY;

		for (PyramideNode child : node.getChildren()) {
			Point childCoord = assocNodes.get(pyramide2D.getNodeHashMap().get(child)).getPosition();

			if (childCoord == null) {
				throw new PyramideViewer2DException("Couldn't calculate x coordinate for " + node.getName() + "(id=" + node.getID() + ")" + "because the child node" + child.getName() + "(id=" + child.getID() + ") has not been handled yet");
			}

			if (xMin > childCoord.x) {
				xMin = (float) childCoord.x;
			}

			if (xMax < childCoord.x) {
				xMax = (float) childCoord.x;
			}
		}

		return (xMin + xMax) / 2;
	}

	/**
	 * Trace links between a node and its children
	 * 
	 * @param g2
	 * @param node
	 * @param x
	 *            node x coordinate
	 * @param y
	 *            node y coordinate
	 */
	protected void traceLinks(Graphics2D g2, PyramideNode node, float x, float y) {

		switch (pyramide2D.getLinkShape()) {

		case TRIANGULAR_LINKS:
			traceTriangularLinks(g2, node, x, y);
			break;

		case TRAPEZOIDAL_LINKS:
			traceTrapezoidalLinks(g2, node, x, y);
			break;

		default:
			traceTriangularLinks(g2, node, x, y);
		}
	}

	/**
	 * Trace the links between a node and its children
	 * 
	 * @param g2
	 * @param node
	 * @param x
	 *            node x coordinate
	 * @param y
	 *            node y coordinate
	 */
	protected void traceTriangularLinks(Graphics2D g2, PyramideNode node, float x, float y) {

		if (node.getChildren() != null) {

			for (PyramideNode child : node.getChildren()) {
				Point childCoord = assocNodes.get(pyramide2D.getNodeHashMap().get(child)).getPosition();

				g2.draw(new Line2D.Float(x + (NODE_WIDTH / 2), y + (NODE_WIDTH), childCoord.x + (NODE_WIDTH / 2), childCoord.y));

			}
		}
	}

	/**
	 * Trace trapezoidal links between a node and its children
	 * 
	 * @param g2
	 * @param node
	 * @param x
	 *            node x coordinate
	 * @param y
	 *            node y coordinate
	 */
	protected void traceTrapezoidalLinks(Graphics2D g2, PyramideNode node, float x, float y) {

		if (node.getChildren() != null) {

			/* Drawing links */
			for (PyramideNode child : node.getChildren()) {
				Point childCoord = assocNodes.get(pyramide2D.getNodeHashMap().get(child)).getPosition();

				/* Drawing the trapezoid */
				if (childCoord.x > x) {
					g2.draw(new Line2D.Float(x + NODE_WIDTH, y + (NODE_WIDTH / 2), childCoord.x + (NODE_WIDTH / 2) - TRAPEZOID_DELTA, y + (NODE_WIDTH / 2)));
					g2.draw(new Line2D.Float(childCoord.x + (NODE_WIDTH / 2) - TRAPEZOID_DELTA, y + (NODE_WIDTH / 2), childCoord.x + (NODE_WIDTH / 2), childCoord.y));
				}
				else {
					g2.draw(new Line2D.Float(x, y + (NODE_WIDTH / 2), childCoord.x + (NODE_WIDTH / 2) + TRAPEZOID_DELTA, y + (NODE_WIDTH / 2)));
					g2.draw(new Line2D.Float(childCoord.x + (NODE_WIDTH / 2) + TRAPEZOID_DELTA, y + (NODE_WIDTH / 2), childCoord.x + (NODE_WIDTH / 2), childCoord.y));
				}
			}
		}
	}

	/**
	 * Getter for the current drawing color
	 * 
	 * @return the current drawing {@link Color}
	 */
	public Color getDrawingColor() {
		return drawingColor;
	}

	/**
	 * Setter for the drawing color
	 * 
	 * @param drawingColor
	 *            the {@link Color} to be set
	 */
	public void setDrawingColor(Color drawingColor) {
		this.drawingColor = drawingColor;
	}

	/**
	 * Getter for the sight field color
	 * 
	 * @return the sight field {@link Color}
	 */
	public Color getSightFieldColor() {
		return sightFieldColor;
	}

	/**
	 * Setter for the sight field color
	 * 
	 * @param sightFieldColor
	 *            the {@link Color} to be set
	 */
	public void setSightFieldColor(Color sightFieldColor) {
		this.sightFieldColor = sightFieldColor;
	}

	/**
	 * Release all the mini-view resources
	 */
	public void dispose() {

		/*
		 * Clearing the lists
		 */
		assocNodes.clear();
		base.clear();

		for (LinkedList<MiniNode2D> level : levels) {
			level.clear();
		}

		levels.clear();
	}
}
